迭代器

迭代器

迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

迭代器有两个基本的方法:iter() 和 next()

数据容器迭代器

通过 next 方法获取迭代器中的元素:next 方法获取最后一个元素之后再调用 next,会报错 StopIteration,你也可以给一个默认值来避免报错

iter_01 = iter("abc")
print(next(iter_01))
print(next(iter_01))
print(next(iter_01))
# next方法获取最后一个元素之后再调用next,会报错 StopIteration
# StopIteration 异常用于标识迭代的完成,防止出现无限循环的情况
# print(next(iter_01))
# 你也可以给一个默认值来避免报错
print(next(iter_01,"我靠"))

输出:

a
b
c
我靠

while + next 实现循环遍历迭代器的元素,注意处理 StopIteration 异常

但是直接使用 for 循环不会报这个错,所以我们更推荐 for 循环遍历迭代器

iter_02 = iter("abcdef")
while True:
    try:
        print(next(iter_02), end=" | ")
    except StopIteration:
        print("迭代结束")
        break

输出:

a | b | c | d | e | f | 迭代结束

我们还可以迭代器他数据容器

# 迭代range对象
iter_03 = iter(range(0, 10))
for x in iter_03:
    print(x, end=" | ")

print()

# 迭代元组
tuple_01 = (*range(0, 10),)
iter_04 = iter(tuple_01)
for x in iter_04:
    print(x, end=" | ")

print()

# 迭代列表
iter_05 = iter([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
for x in iter_05:
    print(x, end=" | ")

print()

输出:

0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 

自定义迭代器类

数据容器之所以能够被迭代,是因为他们默认实现了 __iter__()__next__()

我们也可以自定义一个类来实现这两个方法,通过这种方式,自定义迭代器

class MyIterator:
    def __iter__(self):
        self.a = 1
        return self

    # def __next__(self):
    #     """注意,这种实现是可以无限得带下去的"""
    #     x = self.a
    #     self.a += 1
    #     return x

    def __next__(self):
        if self.a <= 10:
            x = self.a
            self.a += 1
            return x
        else:
            # StopIteration 异常用于标识迭代的完成,防止出现无限循环的情况,在 __next__() 方法中我们可以设置在完成指定循环次数后触发 StopIteration 异常来结束迭代。
            raise StopIteration

简单迭代一下:

myIterator = MyIterator()
myiterObj = iter(myIterator)
for x in myiterObj:
    print(x, end=" | ")

输出:

1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 

生成器 - 等同于自定义迭代器类

注意,这个概念,Java 是没有的

yield 是一个关键字,其作用是,当运行到 yield 语句时,函数的执行将会暂停,并将 yield 后面的表达式的值返回,然后当再次调用yield所在函数的时候,函数会从上次暂停的地方继续执行,直到再次遇到 yield 语句。

带有yield的函数不再是一个普通函数,Python 解释器会将其视为一个 generator(生成器),假设有一个包含 yield 关键字的函数名为 gener(),调用 gener() 看起来像函数调用, 但不会执行 gener 函数,而是返回一个 iterable 对象!然后我们在 for 循环中遍历这个对象,在 for 循环执行时,每次循环都会执行 gener 函数内部的代码,执行到 yield 语句 时,gener 函数就返回一个迭代值,下次迭代时,代码从 yield 语句 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到直接返回,或者再次遇到 yield。

说白了,yield 的作用就是把一个 function 变成一个 generator(如果不包含 yield 关键字,类型为 function,包含了 yield 关键字,类型为 generator),包含 yield 关键字的函数,就是一个生成器函数,调用一个生成器函数,返回的是一个生成器对象,这个生成器对象同时也是一个迭代器对象。由于函数可以指定参数,因此,生成器可以完全实现前面的自定义迭代器类的效果,而且更加的简洁。

yield有点像 break 和 Java 多线程中的 await 方法的效果结合

# 包含yield关键字的函数,是一个生成器函数,一个生成器函数对象,概念上就是一个迭代器,跟迭代器的用法一样

def countdown(n):
    while n > 0:
        yield n
        n -= 1


# 创建生成器对象
# 一个生成器对象,也是一个迭代器对象
generator = countdown(5)
# 输出 <class 'generator'>
# 如果不包含yield 关键字,类型为 function
print(type(generator))

# 通过迭代生成器获取值
print(next(generator))  # 输出: 5
print(next(generator))  # 输出: 4
print(next(generator))  # 输出: 3

# 使用 for 循环迭代生成器
for value in generator:
    print(value)  # 输出: 2 1

输出:

<class 'generator'>
5
4
3
2
1

在一个 generator function 中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代。

# 带有return的生成器
def countdown_with_return(n):
    while n > 0:
        yield n
        n -= 1
        if n == 2:
            # 使用该return则排出错误
            # 抛出错误 StopIteration
            return


generator_with_return = countdown_with_return(3)
# 输出3
print(next(generator_with_return))
# 报错,输出 StopIteration
# print(next(generator_with_return))

由于有 yield 关键字的方法就是生成器,没有的方法就是函数,很难快速判断,但是我们可以通过函数来判断

# 判断是不是生成器

from inspect import isgeneratorfunction


def test_function():
    print("test")


# True
print(isgeneratorfunction(countdown))
# False
print(isgeneratorfunction(test_function))
# False
print(isgeneratorfunction(lambda: print()))

yield 的简单的应用,生成斐波那契数列

# 生成器本质上是一个产生迭代器的函数,作用类似于前面的自定义迭代器类
def fibonacci(n):
    count, a, b = 1, 1, 2
    while count <= n:
        yield a
        a, b = b, a + b
        count += 1


fibonacci_generator = fibonacci(5)
for i in iter(fibonacci_generator):
    print(i, end=" | ")

输出:

1 | 2 | 3 | 5 | 8 | 

让我们回到 yield 关键字最开始的定义,我们会发现,通过 yield 关键字,我们可以把一个方法分成两段运行,第一段是从方法开始到 yield 语句,第二段是从 yield 的下一句到方法末尾,很有意思

def yield_test():
    print("yield before")
    # yield 返回 None
    yield
    print("yield after")


yield_value = yield_test()
next(yield_value)
# 给个默认值,next读取不到值的时候不会爆 StopIteration 错误
next(yield_value,"end")

输出:

yield before
yield after

跟迭代器的不同

迭代器和生成器有什么区别?

参考文档:Python yield 使用浅析 | 菜鸟教程

为了在结果序列长度很大的时候节约内存,我们不能一股脑将所有序列放到一个容器中,而是通过使用迭代器来按需输出结果序列,为了实现这个目的,实际上我们自定义一个迭代器类也可以实现目的,但是效果没有直接在原函数中使用 yield 函数来的简洁

yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用一个迭代器类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。

感觉只要是不太复杂的迭代器,基本上都能改写成 yield 的版本,也推荐使用 yield 的版本。